Skip to content

fix(parts): commit a part before its handoff turn can run (#751) - #772

Merged
Birfy merged 1 commit into
mainfrom
fix/751-commit-part-before-handoff
Oct 22, 2026
Merged

Birfy merged 1 commit into
mainfrom
fix/751-commit-part-before-handoff

Conversation

@Birfy

@Birfy Birfy commented Oct 22, 2026

Copy link
Copy Markdown
Collaborator

Written-by: Dev

Replaces #771: GitHub closed that one when its head branch was renamed (a mistake on my side) and refuses to reopen it after the force-push, so this is the same branch and the same tip.

Fixes #751

Tip SHA: a882ced

Supersedes #756. Its branch is on the author's fork, and this host cannot push there (a ref probe answers 200, a blob create is 404), so #756 cannot get the regression test review required. This branch carries the author's change — Co-authored-by: HugoXK is in the commit — plus that test. #756 can be closed once this one is judged.

Reproduce

core/test/bot-messages.test.js → a part is committed before its handoff turn can run, so its failure is rescued once: four turned-down requests move the Bot on twice. The test holds the first part's commit save open — rt.save, the call moveOn awaits right after it makes the fresh part current — and fails that part's handoff turn while the save is still in flight. That is the window the merge queue kept landing in. On main the failure handler reads a part that is current in memory but not marked rescued, so the second handoff comes out as a third part that is not the rescued one: its note says There was no earlier note to carry over instead of the note of the part before it, and the messages that were supposed to be left out come along.

Nothing waits on a timer: the failure is injected inside the save the product itself awaits, and every assertion is read from the recorded handoff.

What changed

  • core/src/host/parts.js (moveOn): the new segment, the pointer and the rescued mark are set and saved before the handoff package is sent, so the turn the handoff starts cannot see an uncommitted part. The mark goes in before the save awaits — a failure handler that sees this part can never see it unmarked — and restore() drops pixel and mark together when the save fails. A failed handoff rolls the pointer back, saves the old part best effort and archives the fresh Session, so the stopped turn's input still goes on in the old part. moveSchedules stays after the package.
  • core/test/bot-messages.test.js: the case above; it asserts the second handoff is part 3, still says the latest messages are left out this time, lists (none), and that only two parts were made. The harness now builds the runtime with createRuntime so a test can hold that one write open.
  • docs/subsystems/core-host.md: the rule, plus the two windows this ordering leaves open — a crash after the save and before the package is flushed (fresh part current, no package: the work goes on there, the note does not, and the rollback never runs), and a failing rollback save (file shows the fresh part, memory the old one). changelog.d/parts-commit-before-handoff.fixed.md and patches/ds-bot/ (0001, 0014) regenerated from core/.

What I ran

On CI's pair (Node 24.10.0, DSH 0.2.0-rc.2 from npm), TZ=UTC:

  • node --import ./test/harness-resolve.mjs --test test/bot-messages.test.js → 21 tests, 21 pass; test/work-dir.test.js → 13/13.
  • The same file 10 times at --test-concurrency=6 → 21/21 every run.
  • Whole core suite at --test-concurrency=6: 1254 tests, 1251 pass, 3 fail — the three environment reds this box has on main too (empty diff passes, composer-label, read-page; no git, no jsdom here).
  • Patch series: applying patches/ds-bot/files/ and the ten patches to Fei2411/DeepSeek-Bot@2b10c70 reproduces core/ byte for byte (only the local node_modules of this box is missing).

Red without the source change

Two cuts, each restored afterwards (21/21 again after each); the file is green 10 runs at --test-concurrency=6.

  1. core/src/host/parts.js back to main → the new case is red in 97–99 ms, three runs in a row, on the rescued part lost the note of the part before it. Actual: [Handoff · part 3] … There was no earlier note to carry over … ## Latest messages (none); expected /so they are left out this time\\. First tell the user/. The other 20 cases stay green (20 pass / 1 fail). The wait that leads there no longer asserts on its own: it settles, then assert.ok(second, "the second switch was dropped: …") reads as a dropped switch if the handoff never comes, so a red here can never look like a timeout again.
  2. Keeping the commit-before-package order but moving the rescued mark back after the save → the same case fails on the same assertion (23 ms), so the mark — not only the order — is what the handler needs.

Not run

  • The suite as a coverage run, and the client bundle check: no client file changes.
  • Nightly's compatibility matrix.

Risk

T2 (core/src/host/**). Ordering only, and the failure path now rolls back more state (pointer, segments, questions, rescued) and saves it best effort. A failed handoff still leaves the old part current and hands the stopped turn's input on there, which is what work-dir.test.js covers.

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A part move exposes the fresh part before its segment is saved, racing a second switch

1 participant